package com.jse.jdbc;

import java.sql.*;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.naming.InitialContext;
import javax.sql.DataSource;
import com.jse.Fs;
import com.jse.Ioc;
import com.jse.Js;
import com.jse.Lang;
import com.jse.Log;
import com.jse.Refs;
import com.jse.Times;
import com.jse.json.Json;
import com.jse.json.JsonArray;
import com.jse.json.JsonObject;

public class Jdbc {

    static final Log log = Log.get("Jdbc");
    public static boolean showSql = true;
    public static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();
    public static String dialect = "mysql"; // 默认为 MySQL
    public static final String TEST_SQL = "/* ping */";

//    private static final Connection CLOSED_CONNECTION = (Connection) Refs.newProxy(Connection.class, (o, m, a) -> {
//        if ("toString".equals(m.getName())) return "Connection has been closed";
//        else throw new SQLException("No operations allowed after connection closed");
//    });
//
//    private static final Statement CLOSED_STATEMENT = (Statement) Refs.newProxy(Statement.class, (o, m, a) -> {
//        if ("toString".equals(m.getName())) return "Statement has been closed";
//        else throw new SQLException("No operations allowed after statement closed");
//    });
//
//    private static final ResultSet CLOSED_RESULT_SET = (ResultSet) Refs.newProxy(ResultSet.class, (o, m, a) -> {
//        if ("toString".equals(m.getName())) return "ResultSet has been closed";
//        else throw new SQLException("No operations allowed after resultSet closed");
//    });

    /**
     * 初始化数据源。
     * 
     * 从提供的配置对象中读取数据源类型和URL，并根据类型初始化适当的数据源。
     * 支持的数据源类型包括默认的自定义数据源、JNDI 查找以及通过脚本创建的数据源。
     * 
     * @param config 数据源配置信息。
     */
    public static void initDataSource(Object config) {
        var conf = Json.jsonObject(config);
        String type = conf.remove("type", "");
        String url = conf.getString("url");
        dialect = detectDialect(url);
        try {
            Ioc.reg("dataSource", switch (type) {
                case "", "default" -> new JseDataSource(conf);
                case "jndi" -> InitialContext.doLookup(url);
                default -> Js.eval(Fs.readString("jse", "/META-INF/ds/" + type + ".js"), conf);
            });
        } catch (Exception e) {
            log.error("Error initializing data source", e);
        }
    }

    /**
     * 根据给定的数据库 URL 检测数据库方言。
     * 
     * @param url 数据库连接 URL。
     * @return 数据库方言标识符。
     */
    private static String detectDialect(String url) {
        if (url.startsWith("jdbc:mysql") || url.startsWith("jdbc:cobar:") || url.startsWith("jdbc:log4jdbc:mysql:")) {
            return "mysql";
        } else if (url.startsWith("jdbc:postgresql")) {
            return "postgresql";
        } else if (url.startsWith("jdbc:db2")) {
            return "db2";
        } else {
            log.warn("Unsupported database dialect! Defaulting to MySQL.");
        }
        return "mysql";
    }

    /**
     * 获取与数据源相关的数据库连接。
     * 
     * 如果当前线程已经存在连接，则返回该连接；否则，从数据源中获取新的连接。
     * 
     * @param dataSource 数据源实例。
     * @return 数据库连接。
     */
    public static Connection getConnection(DataSource dataSource) {
        try {
        	if(CONNECTION.orElse(null)!=null){return CONNECTION.get();}
            return ScopedValue.where(CONNECTION, dataSource.getConnection()).get(CONNECTION);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取默认数据源的数据库连接。
     * 
     * 此方法通过依赖注入框架获取默认的数据源，并调用 {@link #getConnection(DataSource)} 方法。
     * 
     * @return 数据库连接。
     */
    public static Connection getConnection() {
        return getConnection(Ioc.getBean("dataSource", DataSource.class));
    }

    /**
     * 执行一个事务内的操作，并设置指定的事务隔离级别。
     * 
     * 本方法开启一个事务，在给定的隔离级别下执行指定的任务（`fun`），并确保事务最终被提交或回滚。
     * 如果在执行任务过程中发生异常，则回滚事务并抛出异常。
     * 
     * 隔离级别及其影响：
     * <table border="1">
     *     <tr>
     *         <th>隔离级别</th>
     *         <th>事务支持</th>
     *         <th>脏读</th>
     *         <th>不可重复读</th>
     *         <th>幻影读</th>
     *         <th>实际效果</th>
     *     </tr>
     *     <tr>
     *         <td>0, TRANSACTION_NONE</td>
     *         <td>不支持</td>
     *         <td>不适用</td>
     *         <td>不适用</td>
     *         <td>不适用</td>
     *         <td>无</td>
     *     </tr>
     *     <tr>
     *         <td>2, TRANSACTION_READ_COMMITTED</td>
     *         <td>支持</td>
     *         <td>阻止</td>
     *         <td>允许</td>
     *         <td>允许</td>
     *         <td>读已提交</td>
     *     </tr>
     *     <tr>
     *         <td>1, TRANSACTION_READ_UNCOMMITTED</td>
     *         <td>支持</td>
     *         <td>允许</td>
     *         <td>允许</td>
     *         <td>允许</td>
     *         <td>读未提交</td>
     *     </tr>
     *     <tr>
     *         <td>4, TRANSACTION_REPEATABLE_READ</td>
     *         <td>支持</td>
     *         <td>阻止</td>
     *         <td>阻止</td>
     *         <td>允许</td>
     *         <td>可重复读</td>
     *     </tr>
     *     <tr>
     *         <td>8, TRANSACTION_SERIALIZABLE</td>
     *         <td>支持</td>
     *         <td>阻止</td>
     *         <td>阻止</td>
     *         <td>阻止</td>
     *         <td>串行化</td>
     *     </tr>
     * </table>
     *
     * @param level 事务隔离级别，根据 {@link Connection} 接口定义的常量设置。
     * @param fun 要在事务中执行的操作，类型为 {@link Runnable}。
     */
    public static void transaction(int level, Runnable fun) {
        Connection conn = getConnection();
        try {
            conn.setAutoCommit(false);
            conn.setTransactionIsolation(level);
            ScopedValue.where(CONNECTION, conn).run(fun);
        } catch (Exception e) {
            // 事务的错误处理
            e.printStackTrace();
            rollback(conn);
        } finally {
        	endTransaction(conn);
        }   
    }

    public static void beginTransaction(Connection conn) {
        try {
            conn.setAutoCommit(false);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public static void endTransaction(Connection conn) {
        try {
            conn.commit();
            conn.setAutoCommit(true);
            conn.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public static void rollback(Connection conn) {
        try {
            conn.rollback();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public static void printWarnings(SQLWarning warning) {
        while (warning != null) {
        	log.info("Warning Message: {}, SQL State: {}, Vendor Error Code: {}",
                    warning.getMessage(), warning.getSQLState(), warning.getErrorCode());
            warning = warning.getNextWarning();
        }
    }

    public static void setParameters(PreparedStatement stmt, Object... args) throws SQLException {
        for (int i = 0; i < args.length; i++) {
            Object param = Lang.def(args[i], null);
            if (param == null || param.getClass().getSimpleName().equals("Undefined")) {
                stmt.setObject(i + 1, null);
            } else if (param instanceof Collection || param instanceof Map || param.getClass().isArray()) {
                stmt.setObject(i + 1, Json.toJson(param));
            } else {
                stmt.setObject(i + 1, param);
            }
        }
    }
    public static void setParameters(PreparedStatement stmt,List<Object> args) throws SQLException {
        for (int i = 0; i < args.size(); i++) {
            Object param = Lang.def(args.get(i), null);
            if (param == null || param.getClass().getSimpleName().equals("Undefined")) {
                stmt.setObject(i + 1, null);
            } else if (param instanceof Collection || param instanceof Map 
            		|| param.getClass().isArray()) {
                stmt.setObject(i + 1, Json.toJson(param));
            } else {
                stmt.setObject(i + 1, param);
            }
        }
    }
    
    public static Object whereValue(Object o) {
		if(o==null) {
			return null;
		}else if(o instanceof Number n) {
			return n;
		}else if(o instanceof Character s) {
			return s;
		}else if(o instanceof Boolean s) {
			return s.booleanValue()?1:0;
		}else if(o instanceof String s) {
			return s;
		}else if(o instanceof Date d) {
			return Times.toString(d);
		}else if(o instanceof Collection c) {
			return Json.toJson(c).replace('[','(').replace(']',')')
					.replace('"','\'').replace("true","1").replace("false","0");
		}else if(o.getClass().isArray()) {
			return whereValue(List.of((Object[])o));
		}else if(o.getClass().getSimpleName().equals("Undefined")) {
			return null;
		}
		return o.toString();
	}

    public static void close(AutoCloseable... closables) {
        for (AutoCloseable closable : closables) {
            try {
                if (closable != null) closable.close();
            } catch (Exception e) {
                log.error("Error closing resource", e);
            }
        }
    }

    public static String paginationSQL(Map<String, Object> pager) {
        if (!pager.containsKey("offset")) {
            int offset = Integer.parseInt(pager.get("pageSize").toString()) * (Integer.parseInt(pager.get("pageNo").toString()) - 1);
            pager.put("offset", offset);
        }
        if ("postgresql".equals(dialect)) {
            return " LIMIT %s OFFSET %s".formatted(pager.get("pageSize"), pager.get("offset"));
        }
        return " LIMIT %s, %s".formatted(pager.get("offset"), pager.get("pageSize"));
    }

    public static String paginationSQL(int pageNo, int pageSize) {
    	if(pageSize<0) {
    		if ("postgresql".equals(dialect)||"mysql".equals(dialect)) {
                return " LIMIT "+pageNo;
            }
    		return "其他sql未实现";
    	}
        int offset = pageSize * (pageNo - 1);
        if ("postgresql".equals(dialect)) {
            return " LIMIT %s OFFSET %s".formatted(pageSize,offset);
        }
        return " LIMIT %s, %s".formatted(offset, pageSize);
    }
    
    public static boolean isPrimitiveSql(Class<?> c) {
		return c.isPrimitive()||Refs.isWrapperClass(c)||c==String.class
				||c==Date.class;
	}
}